import datetime
import functools
import os
import re
import shutil

import chardet
def file_walk_through(path, ext,slash_replace=None,ext_replace=None,str_replace=None,exclude_file_list=None,exclude_dir_list=None,ext_type="file",callback=None):
    """
    递归获取路径中指定扩展名的文件路径
    :param path: 路径
    :param ext: 扩展名或者全部扩展名的list
    :param slash_replace: 路径符的替换符号
    :param ext_replace: 扩展名替换符号
    :param str_replace: 字符串替换符(待替换字符串,替换后字符串)
    :param exclude_file_list: 排除文件列表
    :param exclude_dir_list: 排除路径列表
    :param ext_type: 扩展名类型：file->文件，dir->目录
    :param callback: 回调函数用于定制选取符合条件的文件
    :return: 文件路径
    """
    def custom_sort_key(s):
        # 将字符串按数字与非数字部分分割，并转换数字为整数，非数字保持为字符串
        return [(int(part) if part.isdigit() else part) for part in re.split(r'(\d+)', s)]
    def callback_default( root, dirs, files, ext_type):
        ret_files=[]
        if ext_type in ["file"]:
            extfiles=files
        elif ext_type in ["dir"]:
            extfiles=dirs
        else:
            return []
        for file in extfiles:
            if exclude_file_list is not None:
                if file in exclude_file_list:
                    continue
            if exclude_dir_list is not None:
                exc_flag=False
                for exclude_dir_item in exclude_dir_list:
                    if exclude_dir_item in root:
                        exc_flag=True
                        break
                if exc_flag:
                    continue
            if len(os.path.splitext(file)[1][1:])>0 and (os.path.splitext(file)[1] in ext or os.path.splitext(file)[1][1:] in ext):
                replace_file=os.path.join(root,file)
                ret_files.append(replace_file)
        return ret_files
    L_path = []
    # 用来在函数中判断是db还是xls
    print("遍历路径：%s下所有：%s扩展名文件%s" % (path, ext,"夹"if ext_type=="dir" else ""))
    for root, dirs, files in os.walk(path):  # 遍历文件
        # 对子目录和文件分别按 custom_sort_key 排序
        dirs.sort(key=custom_sort_key)
        files.sort(key=custom_sort_key)
        if callback is None:
            replace_files=callback_default( root, dirs, files, ext_type)
        else:
            replace_files=callback( root, dirs, files,ext)
        for replace_file in replace_files:
            if slash_replace is not None:
                replace_file=re.sub(r'[\\/]', slash_replace, replace_file)
            if ext_replace is not None:
                replace_file=os.path.splitext(replace_file)[0]+ext_replace
            if str_replace is not None:
                if len(str_replace)==2:
                    replace_file=re.sub('%s'%str(str_replace[0]), str(str_replace[1]), replace_file)
                else:
                    raise Exception("替换参数错误")
            L_path.append(replace_file)
    return L_path

class TypeChecker:
    @staticmethod
    def type_check(mode="contract"):
        """ 装饰器工厂，先接收 mode 参数，再返回真正的装饰器 """
        def decorator(func):
            if mode == "contract":
                return TypeChecker.contract(func)
            elif mode == "naive":
                return TypeChecker.type_check_naive(func)
            else:
                raise ValueError("mode must be 'contract' or 'naive'")
        return decorator

    @staticmethod
    def type_check_naive(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # 获取函数的注解
            annotations = func.__annotations__
            # 检查形参
            for name, arg in zip(func.__code__.co_varnames, args):
                if name in annotations:
                    expected_type = annotations[name]
                    if not isinstance(arg, expected_type):
                        raise TypeError(f"Argument '{name}' must be of type {expected_type}, got {type(arg)} instead.")
            for name, arg in kwargs.items():
                if name in annotations:
                    expected_type = annotations[name]
                    if not isinstance(arg, expected_type):
                        raise TypeError(f"Argument '{name}' must be of type {expected_type}, got {type(arg)} instead.")
            # 调用函数
            result = func(*args, **kwargs)
            # 检查返回值
            if 'return' in annotations:
                expected_type = annotations['return']
                if not isinstance(result, expected_type):
                    raise TypeError(f"Return value must be of type {expected_type}, got {type(result)} instead.")
            return result
        return wrapper

    @staticmethod
    def contract(func):
        try:
            from contracts import contract
            return contract(func)  # 这里正确返回装饰后的函数
        except ImportError as e:
            raise ImportError(
                f"{e.msg}\r\n本地 contract 不存在，请安装 pycontracts (pip install pycontracts)。\r\n"
                "并且确保:\r\npyparsing==2.2.2\r\n"
                "decorator==4.0.10\r\n"
                "six==1.10.0\r\n"
                "numpy<1.16\r\n"
                "python<3.10"
            ) from e
        except AttributeError as e:
            raise AttributeError(
                f"{e.msg}\r\n安装版 pycontracts 请确保 pyparsing==2.2.2\r\n"
                "decorator==4.0.10\r\n"
                "six==1.10.0\r\n"
                "numpy<1.16\r\n"
                "python<3.10"
            ) from e
class EncodingUtils:
    @staticmethod
    @TypeChecker.type_check()
    def zip_fix_encoding(
            zip_file_path:str,
            output_dir:str=None,
            original_encoding_autodetect:bool=True,
            original_encoding:str='gbk',
            intermediate_encoding:str='cp437',
    ):
        """
        修复 ZIP 文件中的乱码文件名
        :param zip_file_path: ZIP 文件路径
        :param output_dir: 修复后的文件输出目录（默认当前目录）
        :param original_encoding_autodetect: 原始编码是否启用自动检测（默认 True）
        :param original_encoding: 原始正确编码（如 'gbk'）
        :param intermediate_encoding: 如果是双重编码，str→bytes的中间编码（默认 'cp437'）
        """
        def isfile(pathstr:str)->bool:#判断是否是文件样式os.path.isfile()需验证文件存在的真实性
            return bool(re.search(r"\.\w+$", pathstr))

        def zip_coding_fix(srcfile,dstfile):
            if srcfile==dstfile:
                tmpfile=os.path.join(os.path.dirname(dstfile),f"tmp_{datetime.datetime.now()}")
            else:
                tmpfile=dstfile
            import zipfile
            zip_r = zipfile.ZipFile(srcfile, "r")
            if not os.path.exists(os.path.dirname(tmpfile)):
                # **创建 tmpfile 所在目录，避免目录不存在报错**
                os.makedirs(os.path.dirname(tmpfile))
            zip_w = zipfile.ZipFile(tmpfile, "w", zipfile.ZIP_DEFLATED)
            for item in zip_r.infolist():
                file_data=zip_r.open(item.filename)
                dst_filename=EncodingUtils.fix_encoding(item.filename, intermediate_encoding=intermediate_encoding, original_encoding=original_encoding, original_encoding_autodetect=original_encoding_autodetect)
                print("修复压缩文件%s中:(%s -> %s)"%(srcfile,item.filename, dst_filename))
                zip_w.writestr(dst_filename, file_data.read())
                file_data.close()
            zip_w.close()
            zip_r.close()
            shutil.move(tmpfile,dstfile)
            print("修复压缩文件%s修复完成"%(srcfile))
        zip_file_path_=zip_file_path
        zip_filename=None

        if not os.path.exists(zip_file_path):
            raise Exception(f"输入文件或路径：{zip_file_path}不存在")
        if isfile(zip_file_path):
            zip_file_path_,zip_filename=os.path.split(zip_file_path)
        if output_dir is None:
            output_dir = zip_file_path_
        output_dir_=output_dir
        output_filename=None
        if isfile(output_dir):
            output_dir_,output_filename=os.path.split(output_dir)
        if zip_filename is None:#输入若是路径，则遍历路径下压缩文件
            pathfilelist=file_walk_through(zip_file_path_, ["zip"])
            for srcfile in pathfilelist:
                output_filestr = (output_dir + srcfile.replace(zip_file_path_, ""))
                zip_coding_fix(srcfile,output_filestr)
        elif output_filename is None:#输入若是文件,则判断输出路径是否为路径，若是路径，则补充输出路径下文件名

            output_filestr = (os.path.sep).join((output_dir_,zip_filename))
            zip_coding_fix(zip_file_path,output_filestr)
        else:#输入输出均为文件
            zip_coding_fix(zip_file_path,output_dir)
        return []




    @staticmethod
    @TypeChecker.type_check()
    def folder_fix_encoding(
            folder_file_path:str,
            output_dir:str=None,
            original_encoding_autodetect:bool=True,
            original_encoding:str='gbk',
            intermediate_encoding:str='cp437',
    ):
        """
        修复 文件夹中的乱码文件名
        :param folder_file_path: 文件夹路径
        :param output_dir: 修复后的文件输出目录（默认当前目录）
        :param original_encoding_autodetect: 原始编码是否启用自动检测（默认 True）
        :param original_encoding: 原始正确编码（如 'gbk'）
        :param intermediate_encoding: 如果是双重编码，str→bytes的中间编码（默认 'cp437'）
        """

        if output_dir is None:
            output_dir = folder_file_path
        def folder_coding_fix_callback(root, dirs, files, ext_type)->list:

            def folder_coding_fix(srcfile,dstpath):
                _,filename=os.path.split(srcfile)
                if not os.path.exists(dstpath):
                    # **创建 tmpfile 所在目录，避免目录不存在报错**
                    os.makedirs(dstpath)

                dst_filename=EncodingUtils.fix_encoding(filename, intermediate_encoding=intermediate_encoding, original_encoding=original_encoding, original_encoding_autodetect=original_encoding_autodetect)
                dst_pathfile=os.path.join(dstpath,dst_filename)
                print("修复文件:(%s -> %s)"%(srcfile, dst_pathfile))

                shutil.copyfile(srcfile,dst_pathfile)
            for file in files:
                srcfile=os.path.join(root, file)
                output_filestr = (output_dir + srcfile.replace(folder_file_path, ""))
                output_pathstr = os.path.dirname(output_filestr)
                folder_coding_fix(srcfile,output_pathstr)
            return []
        file_walk_through(folder_file_path, "",callback=folder_coding_fix_callback)
    @staticmethod
    @TypeChecker.type_check()
    def fix_encoding(
            mojibake_str:str,
            original_encoding_autodetect:bool=True,
            original_encoding:str='gbk',
            intermediate_encoding:str='cp437',
    )->str:
        """
        修复乱码字符串（支持普通编码错误和双重编码错误）

        :param mojibake_str: 乱码字符串（str 或 bytes）
        :param original_encoding_autodetect: 原始编码是否启用自动检测（默认 True）
        :param original_encoding: 原始正确编码（如 'gbk'）
        :param intermediate_encoding: 如果是双重编码，str→bytes的中间编码（默认 'cp437'）
        :return:  修复后的字符串
        """

        def unicode_to_bytes(mojibake_str):
            # 将每个字符的Unicode码位转为2字节（大端序）
            bytes_data = b''.join(
                ord(c).to_bytes(2, byteorder='big')
                for c in mojibake_str
            )
            return bytes_data
        if isinstance(mojibake_str, str):
            # 双重编码修复：str → bytes → 正确解码
            try:
                # 先用中间编码（如 cp437）转回 bytes
                bytes_data = mojibake_str.encode(intermediate_encoding)
            except UnicodeError as e:
                print(f"双重编码修复失败（中间编码={intermediate_encoding}）: {e},逆向 Unicode 编码转换为原始字节")
                bytes_data = unicode_to_bytes(mojibake_str)
        elif isinstance(mojibake_str, bytes):
            # 如果输入是 bytes，直接尝试解码
            bytes_data = mojibake_str
        else:
            raise TypeError("输入必须是 str 或 bytes")

        chardet_result = chardet.detect(bytes_data)
        chardet_encoding = chardet_result['encoding']
        chardet_confidence = chardet_result['confidence']
        try:
            if chardet_confidence > 0.7 and original_encoding_autodetect:
                return bytes_data.decode(chardet_encoding)
            else:
                warn_str = f"使用默认编码{original_encoding}"
                if original_encoding_autodetect:
                    warn_str=f"检测到的编码 {chardet_encoding} 可能不准确，置信度 {chardet_confidence}\n"+warn_str
                print(warn_str)
                # 再用原始编码解码
                return bytes_data.decode(original_encoding)
        except UnicodeError as e:
            print(f"普通编码修复失败（原始编码={original_encoding}）: {e},返回原始字符串")
            return mojibake_str
if __name__ == '__main__':
    # 示例 1：普通乱码修复（GBK 被错误解码为 CP437）
    mojibake_str = " ─π║├ú¼╩└╜τúí"  # 你好，世界！
    fixed_str = EncodingUtils.fix_encoding(mojibake_str, intermediate_encoding='cp437', original_encoding='gb18030', original_encoding_autodetect=False)
    print(f"修复后: {fixed_str}")

    # 示例 2：zip文件（GBK 被错误解码为 CP437）
    EncodingUtils.zip_fix_encoding(r"F:\testzip\abc\aaa.zip",output_dir=r"F:\testzip1", intermediate_encoding='cp437',original_encoding_autodetect=False,original_encoding='gb18030')
    # 示例 3：文件夹中的zip文件（（GBK 被错误解码为 CP437）
    EncodingUtils.zip_fix_encoding(r"F:\testzip",output_dir=r"F:\testzip1", intermediate_encoding='cp437',original_encoding_autodetect=False,original_encoding='gb18030')

    # 示例 3：文件夹中的乱码文件名文件（（GBK 被错误解码为 CP437）
    EncodingUtils.folder_fix_encoding(r"F:\testfolder",output_dir=r"F:\testfolder2", intermediate_encoding='cp437',original_encoding_autodetect=False,original_encoding='gb18030')


